Skip to content

Implement GMP operator type specifying extension#5223

Open
Firehed wants to merge 8 commits intophpstan:2.1.xfrom
Firehed:gmp-operator-type-extension
Open

Implement GMP operator type specifying extension#5223
Firehed wants to merge 8 commits intophpstan:2.1.xfrom
Firehed:gmp-operator-type-extension

Conversation

@Firehed
Copy link
Contributor

@Firehed Firehed commented Mar 15, 2026

Summary

  • Add GmpOperatorTypeSpecifyingExtension to properly infer return types for GMP operator overloads
  • GMP supports arithmetic (+, -, *, /, %, **), bitwise (&, |, ^, ~, <<, >>), and comparison operators
  • Extension only claims support when both operands are GMP-compatible (GMP, int, or numeric-string)
  • Update InitializerExprTypeResolver to call operator extensions early for object types

Fixes phpstan/phpstan#14288

Test plan

  • Added comprehensive test coverage in tests/PHPStan/Analyser/nsrt/gmp-operators.php
  • Tests cover GMP on both left and right sides of operators
  • Tests cover both operator overloads and corresponding gmp_* functions
  • All existing tests pass (11627 tests, 78892 assertions)

🤖 Generated with Claude Code

@phpstan-bot
Copy link
Collaborator

You've opened the pull request against the latest branch 2.2.x. PHPStan 2.2 is not going to be released for months. If your code is relevant on 2.1.x and you want it to be released sooner, please rebase your pull request and change its target to 2.1.x.

@Firehed Firehed force-pushed the gmp-operator-type-extension branch from a7b2630 to c75bac6 Compare March 15, 2026 19:10
@Firehed Firehed changed the base branch from 2.2.x to 2.1.x March 15, 2026 19:10
Comment on lines +2065 to +2069
$specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry()
->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType);
if ($specifiedTypes !== null) {
return $specifiedTypes;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This become a duplicate with the check later

$specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry()
			->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType);
		if ($specifiedTypes !== null) {
			return $specifiedTypes;
		}

I would either:

  • remove this and rely on the later call
  • or just move the existing call first with condition on object.

$leftType = $getTypeCallback($left);
$rightType = $getTypeCallback($right);

if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't check for Object and be done unconditionally.

$leftType = $getTypeCallback($left);
$rightType = $getTypeCallback($right);

if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't check for Object and be done unconditionally.

$leftType = $getTypeCallback($left);
$rightType = $getTypeCallback($right);

if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't check for Object and be done unconditionally.

Comment on lines +2619 to +2622
// GMP supports unary minus and returns GMP
if ($type->isObject()->yes() && (new ObjectType('GMP'))->isSuperTypeOf($type)->yes()) {
return new ObjectType('GMP');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than an hardcoded check for this, I feel like it we should introduce an UnaryOperatorTypeSpecifyingExtension as explained here: #4980 (comment)

This is maybe better in a dedicated PR ?

$exprType = $getTypeCallback($expr);

// GMP supports bitwise not and returns GMP
if ($exprType->isObject()->yes() && (new ObjectType('GMP'))->isSuperTypeOf($exprType)->yes()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for UnaryOperatorTypeSpecifyingExtension

@Firehed
Copy link
Contributor Author

Firehed commented Mar 15, 2026

Updated to target 2.1 instead of 2.2 per the bot's suggestion. The two failing-as-of-writing tests appear to be failing on other PRs so I don't think they're specific to this change.

I'll check out the other feedback shortly. Thank you for the very quick review!

@ondrejmirtes
Copy link
Member

Please introduce the changes to InitializerExprTypeResolver in a separate PR - this will force you to create synthetic extensions just for test purposes, and review these changes separately, which I want you to do.

Without this, if we once removed the GMP extensions then this feature would become untested.

After that is merged, then you can take advantage of it with new GMP extensions in a new PR.

@Firehed
Copy link
Contributor Author

Firehed commented Mar 15, 2026

Can do. Thanks for the feedback!

@Firehed
Copy link
Contributor Author

Firehed commented Mar 15, 2026

Split per feedback: #5226 contains the InitializerExprTypeResolver changes with synthetic test extensions. Once that's merged, I'll rebase this PR to only contain the GMP-specific extension (or new separate one if you prefer)

@Firehed Firehed force-pushed the gmp-operator-type-extension branch from ade4b49 to 4442956 Compare March 20, 2026 22:38
Copy link
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

Now please again, split this PR in two:

  • One with the GmpOperatorTypeSpecifyingExtension
  • One with the unary minus, unary plus and bitwise not operator

Because for the second one, I think it will be a better API to introduce an UnaryOperatorTypeSpecifyingExtension

@Firehed
Copy link
Contributor Author

Firehed commented Mar 20, 2026

Gotcha, will do. Missed your message before continuing.

@Firehed
Copy link
Contributor Author

Firehed commented Mar 23, 2026

@VincentLanglet #5284 sets up the UnaryOperatorTypeSpecifyingExtension; once that's looking good I'll update the GMP code to build on it instead.

@VincentLanglet
Copy link
Contributor

@VincentLanglet #5284 sets up the UnaryOperatorTypeSpecifyingExtension; once that's looking good I'll update the GMP code to build on it instead.

It's merged, you can now rebase/update this PR :)

Firehed and others added 3 commits March 24, 2026 09:04
This adds comprehensive type inference tests for GMP operations:

- Arithmetic operators (+, -, *, /, %, **) with GMP on left and right
- Bitwise operators (&, |, ^, ~, <<, >>) with GMP on left and right
- Comparison operators (<, <=, >, >=, ==, !=, <=>) with GMP on left and right
- Assignment operators (+=, -=, *=)
- Corresponding gmp_* functions (gmp_add, gmp_sub, gmp_mul, etc.)

These tests currently fail because PHPStan lacks a GmpOperatorTypeSpecifyingExtension
to specify that GMP operations return GMP rather than int|float.

Related: phpstan/phpstan#12123

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add GmpOperatorTypeSpecifyingExtension to properly infer return types
for GMP operator overloads. GMP supports arithmetic (+, -, *, /, %, **),
bitwise (&, |, ^, ~, <<, >>), and comparison (<, <=, >, >=, ==, !=, <=>)
operators.

The extension only claims support when both operands are GMP-compatible
(GMP, int, or numeric-string). Operations with incompatible types like
stdClass are left to the default type inference.

Also update InitializerExprTypeResolver to call operator extensions
early for object types in resolveCommonMath and bitwise methods, and
add explicit GMP handling for unary operators (-$a, ~$a).

Fixes phpstan/phpstan#14288

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements UnaryOperatorTypeSpecifyingExtension for GMP to handle
unary minus (-), unary plus (+), and bitwise NOT (~) operators.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@Firehed Firehed force-pushed the gmp-operator-type-extension branch from aafd495 to eaebe39 Compare March 24, 2026 16:19
@Firehed
Copy link
Contributor Author

Firehed commented Mar 24, 2026

Alright @VincentLanglet I've done the rebase again, and now this is purely the GMP stuff leveraging the type overloading stuff from the previous two split-off PRs. It's still pretty much entirely Claude going at it, but it's now feeling pretty tightly-scoped from my perspective. LMK if there's anything else here :)

- Simplify isOperatorSupported to only check if one side is GMP
- Move compatibility checking to specifyType, returning ErrorType for
  incompatible operands (like stdClass)
- Add unit tests for the extension
- Update pow.php test to expect ErrorType for stdClass ** GMP

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@VincentLanglet
Copy link
Contributor

You just need to fix the build

- Add #[Override] attribute to setUp() method
- Replace match expression with switch for PHP 7.4 compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@Firehed Firehed marked this pull request as draft March 25, 2026 18:17
Firehed and others added 3 commits March 25, 2026 16:44
- Use ObjectWithoutClassType for generic object type (not ObjectType('object'))
- Add test cases for object+GMP to catch IsSuperTypeOfCalleeAndArgumentMutator on line 37
- Add test case for GMP|int+int to catch TrinaryLogicMutator on line 37
- Add test cases for GMP+int|stdClass to catch TrinaryLogicMutator on line 52

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add GMP + string test to catch TrinaryLogicMutator on isNumericString
- Add unary operator tests on object type to catch mutations in GmpUnaryOperatorTypeSpecifyingExtension

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
General string has isNumericString()=Maybe, catching the TrinaryLogicMutator
that changes .yes() to !.no() on line 53.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@Firehed
Copy link
Contributor Author

Firehed commented Mar 26, 2026

Ok, I can't for the life of me figure out the mutation testing issues, even after going through setting it up locally and ensuring it passed there. Perhaps it's somehow specific to 8.3/8.4 vs my 8.5? I will probably need an assist here to get it over the line.

I don't think the Larastan issues are related to this PR based on the errors, though I also don't see it failing on the upstream branch. It's a bit tricky to confirm since CI generates an enormous number of jobs (how do you all get such good parallelism btw? I see my own job matrixes cap out way lower, including for OSS projects)

@ondrejmirtes
Copy link
Member

@Firehed PHPStan org pays for the enterprise plan 😊

@VincentLanglet
Copy link
Contributor

VincentLanglet commented Mar 26, 2026

There is sometimes false positives about mutation testing and larastan is unrelated indeed

so I would say it's ready @Firehed

@Firehed Firehed marked this pull request as ready for review March 26, 2026 17:30
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

@Firehed
Copy link
Contributor Author

Firehed commented Mar 26, 2026

Alright @VincentLanglet - moved out of draft state. LMK if you want any other edits.

Thanks for the info on CI @ondrejmirtes. And thanks to both of you for the support and feedback getting this wrapped up!

@VincentLanglet VincentLanglet requested a review from staabm March 26, 2026 18:27
}

$gmpType = new ObjectType('GMP');
return $gmpType->isSuperTypeOf($operand)->yes();
Copy link
Contributor

@staabm staabm Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the

- return $gmpType->isSuperTypeOf($operand)->yes(); 
+ return !$gmpType->isSuperTypeOf($operand)->no();

mutation can be killed by testing with a "maybe GMP operand", like

function maybeGmp(GMP $a, GMP $b) {
	if (rand(0,1)) {
		$a = 5;
	}
	assertType(..., $a + $b));
}

same for the other extension

@staabm
Copy link
Contributor

staabm commented Mar 27, 2026

running PHPStan on gmp code with this PR leads errors like

  27     Only numeric types are allowed in *, GMP given on the left side.                                     
         🪪  mul.leftNonNumeric                                                                               
         at gmp-operators.php:27                                                                              
  28     Only numeric types are allowed in /, GMP given on the left side.                                     
         🪪  div.leftNonNumeric                                                                               
         at gmp-operators.php:28                                                                              
  29     Only numeric types are allowed in %, GMP given on the left side.                                     
         🪪  mod.leftNonNumeric                                                                               
         at gmp-operators.php:29                                                                              
  30     Only numeric types are allowed in **, GMP given on the left side.                                    
         🪪  pow.leftNonNumeric                                                                               
         at gmp-operators.php:30                                                                              
  33     Only numeric types are allowed in +, GMP given on the right side.                                    
         🪪  plus.rightNonNumeric                                                                             
         at gmp-operators.php:33                                        

  81     Loose comparison via "!=" between GMP and int is not allowed.                                        
         🪪  notEqual.notAllowed                                                                              
         💡  Use strict comparison via "!==" instead.                                                         
         at gmp-operators.php:81                                                                              
  89     Loose comparison via "==" between int and GMP is not allowed.                                        
         🪪  equal.notAllowed                                                                                 
         💡  Use strict comparison via "===" instead.           
         
  105    Only numeric types are allowed in *, GMP given on the left side.                                     
         🪪  mul.leftNonNumeric                                                                               
         at gmp-operators.php:105                                                                             
  181    Only numeric types are allowed in *, GMP given on the left side.                                     
         🪪  mul.leftNonNumeric                                                                               
         at gmp-operators.php:181

repro

cp tests/PHPStan/Analyser/nsrt/gmp-operators.php .
php bin/phpstan analyze gmp-operators.php --debug

@VincentLanglet
Copy link
Contributor

running PHPStan on gmp code with this PR leads errors like

The error are coming from the strict rules no ?
There is a similar PR phpstan/phpstan-strict-rules#305

I would say that the strict rules will need to be updated once the operator type specifying extension is merged

@staabm
Copy link
Contributor

staabm commented Mar 27, 2026

I would say that the strict rules will need to be updated once the operator type specifying extension is merged

I think we should already prepare the strict-rules PR - before merging here.
(so we don't get a release blocker)

@VincentLanglet
Copy link
Contributor

I would say that the strict rules will need to be updated once the operator type specifying extension is merged

I think we should already prepare the strict-rules PR - before merging here. (so we don't get a release blocker)

I don't think it's a release blocker considering this:
https://phpstan.org/r/f47eab0a-5160-4995-b2a6-108a480fb852

The Only numeric types are allowed in +, GMP given on the left side. already exists
The inference is wrong

This PR is fixing the second point without touching the first one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GMP operator overloading false-positive errors

5 participants